use crate::BlockStateId;
use crate::block::entities::BlockEntity;
use crate::chunk::format::LightContainer;
use crate::tick::scheduler::ChunkTickScheduler;
use palette::{BiomePalette, BlockPalette};
use pumpkin_data::block_properties::blocks_movement;
use pumpkin_data::chunk::ChunkStatus;
use pumpkin_data::fluid::Fluid;
use pumpkin_data::tag::Block::MINECRAFT_LEAVES;
use pumpkin_data::tag::Taggable;
use pumpkin_data::{Block, BlockState};
use pumpkin_nbt::compound::NbtCompound;
use pumpkin_nbt::nbt_long_array;
use pumpkin_util::math::{position::BlockPos, vector2::Vector2};
use serde::{Deserialize, Serialize};
use std::ops::{BitAnd, BitOr};
use std::{collections::HashMap, sync::Arc};
use thiserror::Error;

pub mod format;
pub mod io;
pub mod palette;

// TODO
pub const CHUNK_WIDTH: usize = BlockPalette::SIZE;
pub const CHUNK_AREA: usize = CHUNK_WIDTH * CHUNK_WIDTH;
pub const BIOME_VOLUME: usize = BiomePalette::VOLUME;
pub const SUBCHUNK_VOLUME: usize = CHUNK_AREA * CHUNK_WIDTH;

#[derive(Error, Debug)]
pub enum ChunkReadingError {
    #[error("Io error: {0}")]
    IoError(std::io::ErrorKind),
    #[error("Invalid header")]
    InvalidHeader,
    #[error("Region is invalid")]
    RegionIsInvalid,
    #[error("Compression error {0}")]
    Compression(CompressionError),
    #[error("Tried to read chunk which does not exist")]
    ChunkNotExist,
    #[error("Failed to parse chunk from bytes: {0}")]
    ParsingError(ChunkParsingError),
}

#[derive(Error, Debug)]
pub enum ChunkWritingError {
    #[error("Io error: {0}")]
    IoError(std::io::ErrorKind),
    #[error("Compression error {0}")]
    Compression(CompressionError),
    #[error("Chunk serializing error: {0}")]
    ChunkSerializingError(String),
}

#[derive(Error, Debug)]
pub enum CompressionError {
    #[error("Compression scheme not recognised")]
    UnknownCompression,
    #[error("Error while working with zlib compression: {0}")]
    ZlibError(std::io::Error),
    #[error("Error while working with Gzip compression: {0}")]
    GZipError(std::io::Error),
    #[error("Error while working with LZ4 compression: {0}")]
    LZ4Error(std::io::Error),
    #[error("Error while working with zstd compression: {0}")]
    ZstdError(std::io::Error),
}

// Clone here cause we want to clone a snapshot of the chunk so we don't block writing for too long
pub struct ChunkData {
    pub section: ChunkSections,
    /// See `https://minecraft.wiki/w/Heightmap` for more info
    pub heightmap: ChunkHeightmaps,
    pub position: Vector2<i32>,
    pub block_ticks: ChunkTickScheduler<&'static Block>,
    pub fluid_ticks: ChunkTickScheduler<&'static Fluid>,
    pub block_entities: HashMap<BlockPos, Arc<dyn BlockEntity>>,
    pub light_engine: ChunkLight,
    pub status: ChunkStatus,
    pub dirty: bool,
}

#[derive(Clone)]
pub struct ChunkEntityData {
    pub chunk_position: Vector2<i32>,
    pub data: HashMap<uuid::Uuid, NbtCompound>,

    pub dirty: bool,
}

/// Represents pure block data for a chunk.
/// Subchunks are vertical portions of a chunk. They are 16 blocks tall.
/// There are currently 24 subchunks per chunk.
///
/// A chunk can be:
/// - Subchunks: 24 separate subchunks are stored.
#[derive(Debug, Clone)]
pub struct ChunkSections {
    pub sections: Box<[SubChunk]>,
    pub min_y: i32,
}

impl ChunkSections {
    #[cfg(test)]
    pub fn dump_blocks(&self) -> Vec<u16> {
        // TODO: this is not optimal, we could use rust iters
        let mut dump = Vec::new();
        for section in self.sections.iter() {
            section.block_states.for_each(|raw_id| {
                dump.push(raw_id);
            });
        }
        dump
    }

    #[cfg(test)]
    pub fn dump_biomes(&self) -> Vec<u8> {
        // TODO: this is not optimal, we could use rust iters
        let mut dump = Vec::new();
        for section in self.sections.iter() {
            section.biomes.for_each(|raw_id| {
                dump.push(raw_id);
            });
        }
        dump
    }
}

#[derive(Debug, Default, Clone)]
pub struct SubChunk {
    pub block_states: BlockPalette,
    pub biomes: BiomePalette,
}

#[derive(Debug, Default, Clone)]
pub struct ChunkLight {
    pub sky_light: Box<[LightContainer]>,
    pub block_light: Box<[LightContainer]>,
}

#[derive(Debug, Clone, Copy)]
pub enum ChunkHeightmapType {
    WorldSurface = 0,
    MotionBlocking = 1,
    MotionBlockingNoLeaves = 2,
}
impl TryFrom<usize> for ChunkHeightmapType {
    type Error = &'static str;

    fn try_from(value: usize) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(ChunkHeightmapType::WorldSurface),
            1 => Ok(ChunkHeightmapType::MotionBlocking),
            2 => Ok(ChunkHeightmapType::MotionBlockingNoLeaves),
            _ => Err("Invalid usize value for ChunkHeightmapType. The value should be 0~2."),
        }
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
#[serde(rename_all = "UPPERCASE")]
pub struct ChunkHeightmaps {
    #[serde(serialize_with = "nbt_long_array")]
    pub world_surface: Box<[i64]>,
    #[serde(serialize_with = "nbt_long_array")]
    pub motion_blocking: Box<[i64]>,
    #[serde(serialize_with = "nbt_long_array")]
    pub motion_blocking_no_leaves: Box<[i64]>,
}

impl ChunkHeightmaps {
    pub fn set(&mut self, _type: ChunkHeightmapType, pos: BlockPos, min_y: i32) {
        let data = match _type {
            ChunkHeightmapType::WorldSurface => &mut self.world_surface,
            ChunkHeightmapType::MotionBlocking => &mut self.motion_blocking,
            ChunkHeightmapType::MotionBlockingNoLeaves => &mut self.motion_blocking_no_leaves,
        };

        let local_x = (pos.0.x & 15) as usize;
        let local_z = (pos.0.z & 15) as usize;

        let adjust_height = (pos.0.y + min_y.abs()) as usize;

        assert!(adjust_height <= 2 << 9);

        //chunk column index in 16*16 chunk.
        let column_idx = local_z * 16 + local_x;

        // Each height value uses 9 bits, calculate starting bit position
        let bit_start_idx = column_idx * 9;

        // Find where these 9 bits start within a 64-bit packed array element
        // We use bit_start_index % 63, which means the last bit of i64 won't be used,
        // but this avoids the hassle of bit concatenation.
        let packed_array_bit_start_idx = bit_start_idx as u32 % 63;

        let mask = {
            if packed_array_bit_start_idx == 0 {
                //0b0000_0000_0111_1111_...
                !(0x1FF << (64 - 9))
            } else {
                !(0x1FF << (64 - packed_array_bit_start_idx - 9))
            }
        };

        let height_bit_bytes = adjust_height
            .wrapping_shl(64 - 9 - packed_array_bit_start_idx)
            .to_ne_bytes();
        let height = i64::from_ne_bytes(height_bit_bytes);

        let packed_array_idx = column_idx / 7;

        data[packed_array_idx] = data[packed_array_idx].bitand(mask).bitor(height);
    }

    pub fn get_height(&self, _type: ChunkHeightmapType, x: i32, z: i32, min_y: i32) -> i32 {
        let data = match _type {
            ChunkHeightmapType::WorldSurface => &self.world_surface,
            ChunkHeightmapType::MotionBlocking => &self.motion_blocking,
            ChunkHeightmapType::MotionBlockingNoLeaves => &self.motion_blocking_no_leaves,
        };

        let local_x = (x & 15) as usize;
        let local_z = (z & 15) as usize;

        let column_idx = local_z * 16 + local_x;
        let bit_start_idx = column_idx * 9;

        let packed_array_bit_start_idx = bit_start_idx as u32 % 63;

        let mask = {
            if packed_array_bit_start_idx == 0 {
                //0b1111_1111_1000_0000_...
                0x1ff << (64 - 9)
            } else {
                0x1ff << (64 - packed_array_bit_start_idx - 9)
            }
        };

        let packed_array_idx = column_idx / 7;

        let height_bit_bytes_i64 = data[packed_array_idx].bitand(mask).to_ne_bytes();

        (u64::from_ne_bytes(height_bit_bytes_i64)
            .wrapping_shr(64 - (packed_array_bit_start_idx + 9)) as i32)
            - min_y.abs()
    }

    pub fn log_heightmap(&self, _type: ChunkHeightmapType, min_y: i32) {
        let mut header = "Z/X".to_string();
        for x in 0..16 {
            header.push_str(&format!("{x:4}"));
        }

        let grid: String = (0..16)
            .map(|z| {
                let mut row = format!("{z:3}");
                row.push_str(
                    &(0..16)
                        .map(|x| format!("{:4}", self.get_height(_type, x, z, min_y)))
                        .collect::<String>(),
                );
                row
            })
            .collect::<Vec<_>>()
            .join("\n");

        log::info!("\nHeightMap:\n{header}\n{grid}");
    }
}

/// The Heightmap for a completely empty chunk
impl Default for ChunkHeightmaps {
    fn default() -> Self {
        Self {
            // 9 bits per entry
            // 0 packed into an i64 7 times.
            motion_blocking: vec![0; 37].into_boxed_slice(),
            motion_blocking_no_leaves: vec![0; 37].into_boxed_slice(),
            world_surface: vec![0; 37].into_boxed_slice(),
        }
    }
}

impl ChunkSections {
    pub fn new(sections: Box<[SubChunk]>, min_y: i32) -> Self {
        Self { sections, min_y }
    }

    pub fn get_block_absolute_y(
        &self,
        relative_x: usize,
        y: i32,
        relative_z: usize,
    ) -> Option<BlockStateId> {
        let y = y - self.min_y;
        if y < 0 {
            None
        } else {
            let relative_y = y as usize;
            self.get_relative_block(relative_x, relative_y, relative_z)
        }
    }

    pub fn get_rough_biome_absolute_y(
        &self,
        relative_x: usize,
        y: i32,
        relative_z: usize,
    ) -> Option<u8> {
        let y = y - self.min_y;
        if y < 0 {
            None
        } else {
            let relative_y = y as usize;
            self.get_noise_biome(
                relative_y / BlockPalette::SIZE,
                relative_x >> 2 & 3,
                relative_y >> 2 & 3,
                relative_z >> 2 & 3,
            )
        }
    }

    /// Returns the replaced block state ID
    pub fn set_block_absolute_y(
        &mut self,
        relative_x: usize,
        y: i32,
        relative_z: usize,
        block_state: BlockStateId,
    ) -> BlockStateId {
        let y = y - self.min_y;
        debug_assert!(y >= 0);
        let relative_y = y as usize;

        self.set_relative_block(relative_x, relative_y, relative_z, block_state)
    }

    /// Gets the given block in the chunk
    fn get_relative_block(
        &self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
    ) -> Option<BlockStateId> {
        debug_assert!(relative_x < BlockPalette::SIZE);
        debug_assert!(relative_z < BlockPalette::SIZE);

        let section_index = relative_y / BlockPalette::SIZE;
        let relative_y = relative_y % BlockPalette::SIZE;
        self.sections
            .get(section_index)
            .map(|section| section.block_states.get(relative_x, relative_y, relative_z))
    }

    /// Sets the given block in the chunk, returning the old block state ID
    #[inline]
    pub fn set_relative_block(
        &mut self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
        block_state_id: BlockStateId,
    ) -> BlockStateId {
        // TODO @LUK_ESC? update the heightmap
        self.set_block_no_heightmap_update(relative_x, relative_y, relative_z, block_state_id)
    }

    /// Sets the given block in the chunk, returning the old block
    /// Contrary to `set_block` this does not update the heightmap.
    ///
    /// Only use this if you know you don't need to update the heightmap
    /// or if you manually set the heightmap in `empty_with_heightmap`
    pub fn set_block_no_heightmap_update(
        &mut self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
        block_state_id: BlockStateId,
    ) -> BlockStateId {
        debug_assert!(relative_x < BlockPalette::SIZE);
        debug_assert!(relative_z < BlockPalette::SIZE);

        let section_index = relative_y / BlockPalette::SIZE;
        let relative_y = relative_y % BlockPalette::SIZE;
        if let Some(section) = self.sections.get_mut(section_index) {
            return section
                .block_states
                .set(relative_x, relative_y, relative_z, block_state_id);
        }
        0
    }

    pub fn set_relative_biome(
        &mut self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
        biome_id: u8,
    ) {
        debug_assert!(relative_x < BiomePalette::SIZE);
        debug_assert!(relative_z < BiomePalette::SIZE);

        let section_index = relative_y / BiomePalette::SIZE;
        let relative_y = relative_y % BiomePalette::SIZE;
        if let Some(section) = self.sections.get_mut(section_index) {
            section
                .biomes
                .set(relative_x, relative_y, relative_z, biome_id);
        }
    }

    pub fn get_noise_biome(
        &self,
        index: usize,
        scale_x: usize,
        scale_y: usize,
        scale_z: usize,
    ) -> Option<u8> {
        debug_assert!(scale_x < BiomePalette::SIZE);
        debug_assert!(scale_z < BiomePalette::SIZE);
        self.sections
            .get(index)
            .map(|section| section.biomes.get(scale_x, scale_y, scale_z))
    }
}

impl ChunkData {
    /// Gets the given block in the chunk
    #[inline]
    pub fn get_relative_block(
        &self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
    ) -> Option<BlockStateId> {
        self.section
            .get_relative_block(relative_x, relative_y, relative_z)
    }

    /// Sets the given block in the chunk
    #[inline]
    pub fn set_relative_block(
        &mut self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
        block_state_id: BlockStateId,
    ) {
        // TODO @LUK_ESC? update the heightmap
        self.section
            .set_relative_block(relative_x, relative_y, relative_z, block_state_id);
    }

    /// Sets the given block in the chunk, returning the old block
    /// Contrary to `set_block` this does not update the heightmap.
    ///
    /// Only use this if you know you don't need to update the heightmap
    /// or if you manually set the heightmap in `empty_with_heightmap`
    #[inline]
    pub fn set_block_no_heightmap_update(
        &mut self,
        relative_x: usize,
        relative_y: usize,
        relative_z: usize,
        block_state_id: BlockStateId,
    ) {
        self.section
            .set_relative_block(relative_x, relative_y, relative_z, block_state_id);
    }

    //TODO: Tracking heightmaps update.
    pub fn calculate_heightmap(&mut self) -> ChunkHeightmaps {
        let highest_non_empty_subchunk = self.get_highest_non_empty_subchunk();
        let mut heightmaps = ChunkHeightmaps::default();

        for x in 0..16 {
            for z in 0..16 {
                self.populate_heightmaps(&mut heightmaps, highest_non_empty_subchunk, x, z);
            }
        }

        // log::info!("WorldSurface:");
        // heightmaps.log_heightmap(ChunkHeightmapType::WorldSurface, self.section.min_y);
        // log::info!("MotionBlocking:");
        // heightmaps.log_heightmap(ChunkHeightmapType::MotionBlocking, self.section.min_y);
        // log::info!("min_y: {}", self.section.min_y);
        heightmaps
    }

    #[inline]
    fn populate_heightmaps(
        &self,
        heightmaps: &mut ChunkHeightmaps,
        start_sub_chunk: usize,
        x: usize,
        z: usize,
    ) {
        let start_height = (start_sub_chunk as i32) * 16 - self.section.min_y.abs() + 15;
        let mut has_found = [false, false, false];

        for y in (self.section.min_y..=start_height).rev() {
            let pos = BlockPos::new(x as i32, y, z as i32);
            let state_id = self.section.get_block_absolute_y(x, y, z).unwrap();
            let block_state = BlockState::from_id(state_id);
            let block = Block::from_state_id(state_id);

            if !block_state.is_air() && !has_found[ChunkHeightmapType::WorldSurface as usize] {
                heightmaps.set(ChunkHeightmapType::WorldSurface, pos, self.section.min_y);
                has_found[ChunkHeightmapType::WorldSurface as usize] = true;
            }

            let is_motion_blocking = blocks_movement(block_state)
                || Fluid::from_registry_key(block.registry_key())
                    .is_some_and(|fluid| !fluid.states.is_empty());

            if !has_found[ChunkHeightmapType::MotionBlocking as usize] && is_motion_blocking {
                heightmaps.set(ChunkHeightmapType::MotionBlocking, pos, self.section.min_y);
                has_found[ChunkHeightmapType::MotionBlocking as usize] = true;
            }

            if !has_found[ChunkHeightmapType::MotionBlockingNoLeaves as usize]
                && is_motion_blocking
                && !block.has_tag(&MINECRAFT_LEAVES)
            {
                heightmaps.set(
                    ChunkHeightmapType::MotionBlockingNoLeaves,
                    pos,
                    self.section.min_y,
                );
                has_found[ChunkHeightmapType::MotionBlockingNoLeaves as usize] = true;
            }

            if !has_found.contains(&false) {
                return;
            }
        }

        let pos = BlockPos::new(x as i32, self.section.min_y, z as i32);
        for (idx, is_set) in has_found.iter().enumerate() {
            if !(*is_set) {
                heightmaps.set(idx.try_into().unwrap(), pos, self.section.min_y);
            }
        }
    }

    pub fn get_highest_non_empty_subchunk(&self) -> usize {
        for (i, sub_chunk) in self.section.sections.iter().enumerate().rev() {
            if sub_chunk.block_states.non_air_block_count() != 0 {
                return i;
            }
        }
        0
    }
}

#[derive(Error, Debug)]
pub enum ChunkParsingError {
    #[error("Failed reading chunk status {0}")]
    FailedReadStatus(pumpkin_nbt::Error),
    #[error("The chunk isn't generated yet")]
    ChunkNotGenerated,
    #[error("Error deserializing chunk: {0}")]
    ErrorDeserializingChunk(String),
}

#[derive(Error, Debug)]
pub enum ChunkSerializingError {
    #[error("Error serializing chunk: {0}")]
    ErrorSerializingChunk(pumpkin_nbt::Error),
}
